fix: sync query_params on browser back/forward navigation#9490
fix: sync query_params on browser back/forward navigation#9490kimjune01 wants to merge 5 commits into
Conversation
Fixes marimo-team#4153. When users navigate using browser back/forward buttons, query_params now updates and dependent cells re-execute. Changes: - Add UpdateQueryParamsCommand to sync URL state with kernel - Add popstate listener to detect browser navigation - Add /api/kernel/update_query_params endpoint - Update QueryParams state triggers cell re-execution via State system The fix leverages marimo's existing State system: when the browser URL changes via popstate, the frontend sends the new params to the kernel, which updates the QueryParams state object, triggering re-execution of cells that depend on query_params.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
All contributors have signed the CLA ✍️ ✅ |
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
No issues found across 11 files
Architecture diagram
sequenceDiagram
participant Browser as Browser (User)
participant FE as Frontend Runtime (useMarimoKernelConnection)
participant Router as URL Router (pushState/popstate)
participant RequestClient as Request Client (sendUpdateQueryParams)
participant API as API Server (/api/kernel/update_query_params)
participant Kernel as Kernel Runtime (control handler)
participant QueryParams as QueryParams State
participant Scheduler as Cell Scheduler
Note over Browser,Scheduler: Browser Back/Forward Navigation → Query Param Sync
Browser->>Router: browser back/forward click
Router->>Router: URL changes (popstate event)
Router->>FE: popstate event fires
FE->>FE: parseQueryParams() from window.location.href
FE->>RequestClient: sendUpdateQueryParams({ queryParams: {...} })
alt Static/Wasm (Islands or Pyodide)
RequestClient->>RequestClient: putControlRequest(type: "update-query-params")
else Server mode
RequestClient->>API: POST /api/kernel/update_query_params
API->>API: @requires("read") permission check
API->>Kernel: dispatchControlRequest(UpdateQueryParamsRequest)
end
Kernel->>Kernel: handle_update_query_params()
Kernel->>QueryParams: clear() + update(request.query_params)
QueryParams->>QueryParams: _set_value() triggers state change
QueryParams->>Scheduler: state update notification
Scheduler->>Scheduler: _run_cells(set()) re-executes dependent cells
Scheduler->>Kernel: broadcastNotification(CompletedRunNotification)
Kernel-->>API: SuccessResponse
API-->>RequestClient: 200 OK
RequestClient-->>FE: null (void)
FE-->>Browser: UI updates with new cell values
Note over Browser,Kernel: Same pattern as set_ui_element_value
|
I have read the CLA Document and I hereby sign the CLA |
… wiring The new update_query_params endpoint was missing from the OpenAPI schema generation (MODELS list) and the generated TypeScript types. Also missing from the frontend mock, lazy-request proxy, and toasting wrapper—causing TypeScript build failures.
Resolves conflict with PR marimo-team#9577 (refactor: split kernel command dispatch into router + callback bundles). The request_handler cached_property was extracted from runtime.py into kernel_request_handlers.py — wire the UpdateQueryParamsCommand through the new KernelRequestHandlers.register path instead of the old in-line handler.register block.
There was a problem hiding this comment.
Pull request overview
This PR fixes stale mo.query_params() state when users navigate via browser back/forward by adding a frontend popstate listener that sends the current URL query params to the kernel, plus a new backend control endpoint/command to apply the update and re-run dependent cells.
Changes:
- Add
/api/kernel/update_query_paramsendpoint plus OpenAPI schema/types for anUpdateQueryParams*request/command. - Register and handle a new
UpdateQueryParamsCommandin the kernel control request router to update query param state and trigger re-execution. - Add a frontend
popstatelistener and wiresendUpdateQueryParamsthrough all request clients/bridges (network, wasm, islands, mocks).
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/openapi/src/api.ts | Adds generated TS types for the new update_query_params endpoint and schemas. |
| packages/openapi/api.yaml | Defines OpenAPI path + schemas for updating query params from browser navigation. |
| marimo/_server/models/models.py | Adds UpdateQueryParamsRequest model to convert request payload into a runtime command. |
| marimo/_server/api/endpoints/execution.py | Exposes /update_query_params endpoint and dispatches it as a control request. |
| marimo/_runtime/kernel_request_handlers.py | Registers/implements kernel-side handler for UpdateQueryParamsCommand. |
| marimo/_runtime/commands.py | Introduces UpdateQueryParamsCommand and adds it to the command union. |
| marimo/_cli/development/commands.py | Includes new command/request in schema generation list. |
| frontend/src/core/websocket/useMarimoKernelConnection.tsx | Adds popstate listener and sends query param updates to the backend. |
| frontend/src/core/wasm/bridge.ts | Implements sendUpdateQueryParams for the Pyodide bridge. |
| frontend/src/core/network/types.ts | Extends RunRequests and exports UpdateQueryParamsRequest type. |
| frontend/src/core/network/requests-toasting.tsx | Adds toast message mapping for sendUpdateQueryParams errors. |
| frontend/src/core/network/requests-static.ts | Adds static-mode no-op implementation for sendUpdateQueryParams. |
| frontend/src/core/network/requests-network.ts | Adds network request implementation calling /api/kernel/update_query_params. |
| frontend/src/core/network/requests-lazy.ts | Ensures sendUpdateQueryParams waits for connection open like other run requests. |
| frontend/src/core/kernel/queryParamHandlers.ts | Adds parseQueryParams() helper used by the new popstate path. |
| frontend/src/core/islands/bridge.ts | Implements sendUpdateQueryParams for islands/worker bridge. |
| frontend/src/mocks/requests.ts | Updates request client mocks to include sendUpdateQueryParams. |
Comments suppressed due to low confidence (1)
marimo/_runtime/kernel_request_handlers.py:145
- This handler always clears/updates
query_paramsand calls_set_value, which registers a state update even when the incoming params are identical, triggering unnecessary cell re-execution. Compare the current params torequest.query_paramsand early-return (or skip_set_value/_run_cells) when there is no change.
k = self._kernel
k.query_params._params.clear()
k.query_params._params.update(request.query_params)
k.query_params._set_value(k.query_params._params)
if k.state_updates:
await k._run_cells(set())
broadcast_notification(CompletedRunNotification())
| k.query_params._params.clear() | ||
| k.query_params._params.update(request.query_params) |
| const url = new URL(window.location.href); | ||
| const params: Record<string, string | string[]> = {}; | ||
|
|
||
| for (const [key, value] of url.searchParams.entries()) { | ||
| const existing = params[key]; | ||
| if (existing === undefined) { | ||
| params[key] = value; | ||
| } else if (Array.isArray(existing)) { | ||
| existing.push(value); | ||
| } else { | ||
| params[key] = [existing, value]; | ||
| } | ||
| } | ||
|
|
||
| return params; |
| // Listen for browser back/forward navigation to update query params | ||
| useEffect(() => { | ||
| const handlePopState = () => { | ||
| const queryParams = parseQueryParams(); | ||
| // Import at runtime to avoid circular dependency | ||
| import("../network/requests").then(({ getRequestClient }) => { | ||
| const client = getRequestClient(); | ||
| void client.sendUpdateQueryParams({ queryParams }); | ||
| }); | ||
| }; | ||
|
|
||
| window.addEventListener("popstate", handlePopState); | ||
| return () => window.removeEventListener("popstate", handlePopState); | ||
| }, []); |
|
Cannot validate on current setup, so drafting. Please close or take it over. |
Fixes #4153
When users navigate with browser back/forward buttons, the URL changes but
mo.query_paramsdoesn't update, leaving cells stale. The frontend tracks URL changes viapushStatewhen cells update query params, but the reverse path (browser navigation → kernel update) was missing.Fix
Add a
popstatelistener inuseMarimoKernelConnectionthat parses the new query params and sends them to the kernel via a newupdate_query_paramsendpoint. The kernel clears and updates itsquery_paramsstate, then re-runs dependent cells.Backend wiring follows the same pattern as
set_ui_element_value—@requires("read")permission, command dispatched through the kernel's control request handler.